Module modules.frames
File name: framse.py Author: Martin Jůda Python Version: 3.7 Description: GUI frames and their settings
Expand source code
"""
File name: framse.py
Author: Martin Jůda
Python Version: 3.7
Description: GUI frames and their settings
"""
import random
import time
import tkinter as tk
from base64 import b64decode
from datetime import datetime, timedelta
from threading import Thread
from tkinter import font as tkFont
import MFRC522.MFRC522 as MFRC522 # dependency: https://github.com/lthiery/SPI-Py
import RPi.GPIO as GPIO
from PIL import ImageTk, Image
from modules.config import (
NUMBER_OF_QUESTIONS,
DEVICE_ID,
USER_INACTIVITY_SECONDS,
ERROR_TEXT,
QUESTION_SWITCH_SECONDS,
DOOR_OPEN_SECONDS,
RELAY_OUTPUT_PIN,
)
from modules.constants import QUESTIONS_URL, Errors, PICTURE_URL
from modules.shared import (
AsyncBackendCommunicator,
ActionInterface,
ReadCardError,
)
def read_card_id():
"""Read Card ID from sensor
Returns:
str: Card number
Raises:
ReadCardError: when card is not presented
"""
card_reader = MFRC522.MFRC522()
(status, TagType) = card_reader.MFRC522_Request(card_reader.PICC_REQIDL)
if status != card_reader.MI_OK:
card_reader.close_spi()
raise ReadCardError("Error when reading card.")
(status, uid) = card_reader.MFRC522_Anticoll()
card_reader.close_spi()
if status == card_reader.MI_OK:
return f"{uid[0]:02x}{uid[1]:02x}{uid[2]:02x}{uid[3]:02x}".upper()
raise ReadCardError("Error when reading card.")
def read_card(read_card_label, controller):
"""Read card periodic controller, read card and if is not presented plan another
read. If card number is red then frame is switched.
Args:
read_card_label (tk.Label): Label for which is planned after action
controller (GUI): class for switch to another frame
"""
try:
card_number = read_card_id()
except ReadCardError:
read_card_label.after(200, read_card, read_card_label, controller)
else:
controller.switch_frame_by_class(VerifyCardPage, card_number=card_number)
return
class ReadCardPage(tk.Frame, ActionInterface):
"""Read card page"""
def __init__(self, parent, *args, **kwargs):
super(ReadCardPage, self).__init__(parent)
self.parent = parent
self.read_card_label = tk.Label( # Read label
self,
text="Přiložte vstupní kartu",
fg="black",
font=tkFont.Font(family="Helvetica", size=45, weight="bold"),
)
# Center label position and grid settings
self.read_card_label.grid(column=1, row=1, sticky="nsew")
self.grid_rowconfigure(1, weight=1)
self.grid_columnconfigure(1, weight=1)
def post_init_actions(self):
"""Plan periodic card read"""
read_card(self.read_card_label, self.parent)
class VerifyCardPage(tk.Frame, ActionInterface):
"""Verify card number page"""
def __init__(self, parent, *args, **kwargs):
super(VerifyCardPage, self).__init__(parent)
self.parent = parent
self.card_number = kwargs.get("card_number")
# Info label
self.verify_card_label = tk.Label(
self,
text="Karta načtena. Probíhá ověřování její platnosti.",
fg="black",
font=tkFont.Font(family="Helvetica", size=45, weight="bold"),
wraplength=1000,
justify=tk.CENTER,
)
self.verify_card_label.grid(column=1, row=1)
self.grid_rowconfigure(1, weight=1)
self.grid_columnconfigure(1, weight=1)
def post_init_actions(self):
"""Start async backend call and start monitoring if this operation finish"""
backend_thread = AsyncBackendCommunicator(
url=QUESTIONS_URL,
daemon=True,
params=dict(
card_number=self.card_number,
question_count=NUMBER_OF_QUESTIONS,
device_id=DEVICE_ID,
),
)
backend_thread.start()
self.monitor_backend_thread(backend_thread)
def monitor_backend_thread(self, thread):
"""Check if async thread is finished. If it is then handle result and switch
frame if it is not finished then plan periodic result check.
Args:
thread (AsyncBackendCommunicator): Started backend call thread
"""
if thread.is_alive():
# thread still working, plan periodic check after 100ms
self.after(100, lambda: self.monitor_backend_thread(thread))
else:
if thread.is_result_ok:
# thread finished and questions were downloaded
if not thread.result.get("questions"):
self.parent.switch_frame_by_class(
ErrorPage,
error_type=Errors.NO_QUESTIONS_ERROR,
)
return
# init questions container and show first question
questions_container = QuestionsContainer(
controller=self.parent,
student_name=thread.result["student_name"],
questions=thread.result["questions"],
)
questions_container.show_next_question()
return
else:
# thread finished with error
if thread.error_code == 404:
error_type = Errors.NO_ACTIVE_STUDY_ERROR
elif thread.error_code == 403:
error_type = Errors.NO_ACCESS_ERROR
else:
error_type = thread.error_type
# show error page
self.parent.switch_frame_by_class(
ErrorPage,
error_type=error_type,
error_code=thread.error_code,
)
return
def check_user_activity(active_page, ui_element=None, seconds=None):
"""Test if frame was changed by user and if timeout expired
Args:
active_page: frame page instance
ui_element: optional ui element for text change
seconds: inactivity timeout
"""
if active_page.user_did_action:
# user did action then finish periodic check
return
else:
# test if timeout expired
if datetime.now() > active_page.init_time + timedelta(
seconds=USER_INACTIVITY_SECONDS
):
# switch to start page
active_page.parent.switch_frame_by_class(ReadCardPage)
else:
# change text and plan next activity test
if ui_element and seconds:
seconds -= 1
ui_element.config(text=str(seconds))
active_page.after(
1000, check_user_activity, active_page, ui_element, seconds
)
class ErrorPage(tk.Frame, ActionInterface):
"""Error message frame"""
def __init__(self, parent, *args, **kwargs):
super(ErrorPage, self).__init__(parent)
self.parent = parent
self.user_did_action = False
self.init_time = None
# configure grid layout
self.grid_columnconfigure(0, weight=1)
self.grid_columnconfigure(1, weight=1)
for i in range(5):
self.grid_rowconfigure(i, weight=1, uniform="r")
# set error type and labels positions
error_texts = ERROR_TEXT[kwargs["error_type"].value]
error_type_value_label = self.create_label(
f"{error_texts['error_name'].format(error_code=kwargs.get('error_code'))}"
)
error_type_key_label = self.create_label("Typ chyby:")
error_type_key_label.grid(column=0, row=0, stick="e", ipadx=50, ipady=50)
error_type_value_label.grid(column=1, row=0, stick="w", ipadx=50, ipady=50)
next_step_value_label = self.create_label(f"{error_texts['hint']}")
next_step_value_label.grid(column=1, row=1, stick="w", ipadx=50, ipady=50)
nex_step_key_label = self.create_label("Postup:")
nex_step_key_label.grid(column=0, row=1, stick="e", ipadx=50, ipady=50)
contact_value_label = self.create_label(f"{error_texts['contact']}")
contact_value_label.grid(column=1, row=2, stick="w", ipadx=50, ipady=50)
contact_key_label = self.create_label("Kontakt:")
contact_key_label.grid(column=0, row=2, stick="e", ipadx=50, ipady=50)
device_id_value_label = self.create_label(f"{DEVICE_ID}")
device_id_value_label.grid(column=1, row=3, stick="w", ipadx=50, ipady=50)
device_id_key_label = self.create_label("ID zařízení:")
device_id_key_label.grid(column=0, row=3, stick="e", ipadx=50, ipady=50)
exit_button = tk.Button(
self,
text="Zavřít",
command=lambda: self.close_window(),
padx=15,
pady=10,
font=tkFont.Font(family="Helvetica", size=22),
)
exit_button.grid(column=0, row=4, ipadx=30, ipady=30, columnspan=2, stick="n")
def create_label(self, text):
"""Create label with given text
Args:
text(str): Text in label
Returns (tk.Label): configured label
"""
return tk.Label(
self,
text=text,
fg="black",
wraplength=870,
justify=tk.CENTER,
font=tkFont.Font(family="Helvetica", size=22),
)
def close_window(self):
"""Exit button action, set user activity and switch frame"""
self.user_did_action = True
self.parent.switch_frame_by_class(ReadCardPage)
def post_init_actions(self):
"""Set init time for user inactivity and start monitoring of this activity"""
self.init_time = datetime.now()
self.after(1000, check_user_activity, self)
class QuestionPage(tk.Frame, ActionInterface):
"""Question page"""
def __init__(
self,
parent,
questions_container,
question_text,
subject_name,
picture_id,
answers,
is_last_question,
*args,
**kwargs,
):
super(QuestionPage, self).__init__(parent)
self.parent = parent
self.picture_id = picture_id
self.is_last_question = is_last_question
self.questions_container = questions_container
self.user_did_action = False
self.init_time = None
self.picture = None
self.picture_label = None
self.picture_thread = None
# set two column
self.grid_columnconfigure(0, weight=1, uniform="a")
self.grid_columnconfigure(1, weight=1, uniform="a")
if picture_id:
# start picture download
self.picture_thread = AsyncBackendCommunicator(
url=PICTURE_URL.format(picture_id=picture_id), daemon=True
)
self.picture_thread.start()
# set row layout for question with picture
self.grid_rowconfigure(0, weight=1, uniform="b")
self.grid_rowconfigure(1, weight=4, uniform="b")
self.grid_rowconfigure(2, weight=10, uniform="b")
self.grid_rowconfigure(3, weight=1, uniform="b")
self.grid_rowconfigure(4, weight=4, uniform="b")
self.grid_rowconfigure(5, weight=4, uniform="b")
else:
# set row layout for question without picture
self.grid_rowconfigure(0, weight=1, uniform="b")
self.grid_rowconfigure(1, weight=8, uniform="b")
self.grid_rowconfigure(2, weight=1, uniform="b")
self.grid_rowconfigure(3, weight=4, uniform="b")
self.grid_rowconfigure(4, weight=4, uniform="b")
self.questions_mapping = {}
for i in range(4):
# set answers by button numbers
self.questions_mapping[i] = answers[i]
self.student_subject_label = tk.Label(
self,
text=f"{self.questions_container.student_name} - {subject_name}",
fg="black",
font=tkFont.Font(family="Helvetica", size=18, weight="bold"),
padx=20,
wraplength=1100,
justify=tk.CENTER,
)
self.student_subject_label.grid(column=0, row=0, columnspan=2, sticky="nsew")
self.question_text_label = tk.Label(
self,
text=question_text,
fg="black",
font=tkFont.Font(family="Helvetica", size=22, weight="bold"),
wraplength=1100,
justify=tk.CENTER,
bg="white",
)
self.question_text_label.grid(column=0, row=1, columnspan=2, sticky="nsew")
self.result_text = tk.Label(
self,
text=USER_INACTIVITY_SECONDS,
fg="black",
font=tkFont.Font(family="Helvetica", size=22, weight="bold"),
wraplength=1000,
justify=tk.CENTER,
bg="white",
)
self.result_text.grid(
column=0, row=3 if self.picture_id else 2, columnspan=2, sticky="nsew"
)
if self.picture_id:
self.picture_label = tk.Label(self)
self.picture_label.grid(column=0, row=2, columnspan=2, sticky="nsew")
# set buttons with their texts and actions
self.button_0 = self.create_button(0, self.questions_mapping[0]["answer"])
self.questions_mapping[0]["button"] = self.button_0
self.button_0.grid(column=0, row=4 if self.picture_id else 3, sticky="nsew")
self.button_1 = self.create_button(1, self.questions_mapping[1]["answer"])
self.questions_mapping[1]["button"] = self.button_1
self.button_1.grid(column=0, row=5 if self.picture_id else 4, sticky="nsew")
self.button_2 = self.create_button(2, self.questions_mapping[2]["answer"])
self.questions_mapping[2]["button"] = self.button_2
self.button_2.grid(column=1, row=4 if self.picture_id else 3, sticky="nsew")
self.button_3 = self.create_button(3, self.questions_mapping[3]["answer"])
self.button_3.grid(column=1, row=5 if self.picture_id else 4, sticky="nsew")
self.questions_mapping[3]["button"] = self.button_3
def create_button(self, button_id, text):
"""Create configured button
Args:
button_id (int): button id
text (str): button text
Returns:
tk.Button: Button with text and set click action
"""
return tk.Button(
self,
text=text,
command=lambda: self.handle_button_click(button_id),
wraplength=500,
font=tkFont.Font(family="Helvetica", size=16),
justify=tk.CENTER,
highlightbackground="white",
padx=5,
pady=5,
)
def handle_button_click(self, button_number):
"""Handle button click, disable all buttons and set colors
Args:
button_number (int): id of pressed button
"""
self.user_did_action = True # set flag for user action
if self.questions_mapping[button_number]["correct"] is True:
# correct answer pressed, open door
self.disable_buttons()
self.open_door(text="Správná odpověď, dveře otevřeny.")
else:
# set red color for wrong answer
(self.questions_mapping[button_number]["button"]).config(
bg="red", highlightbackground="red"
)
self.disable_buttons()
if self.is_last_question:
# last question, open door
self.open_door(text="Chybná odpověď, zlepšete se. Dveře otevřeny.")
else:
# show next question
self.render_result_label(text="Chybná odpověď, následuje další otázka.")
self.after(
(QUESTION_SWITCH_SECONDS * 1000),
lambda: self.questions_container.show_next_question(),
)
def disable_buttons(self):
"""Disable click for all buttons and set green color for correct answer"""
for i, _ in enumerate(self.questions_mapping):
(self.questions_mapping[i]["button"]).config(
state="disabled", disabledforeground="black"
)
if self.questions_mapping[i]["correct"] is True:
(self.questions_mapping[i]["button"]).config(
bg="green", highlightbackground="green"
)
def open_door(self, text):
"""Open door and set result text and plan frame switch"""
self.render_result_label(text=text)
self.after(
(QUESTION_SWITCH_SECONDS * 1000),
lambda: self.parent.switch_frame_by_class(ReadCardPage),
)
OpenDoor().start()
def render_result_label(self, text):
"""Set text for result label
Args:
text (str): text in label
"""
self.result_text.config(text=text)
def post_init_actions(self):
"""Render picture if question contain it and set init time and start user
inactivity timeout"""
if self.picture_id:
if self.picture_thread.result is None:
# question download failed
if self.is_last_question:
# last question so show error
self.parent.switch_frame_by_class(
ErrorPage,
error_type=Errors.IMAGE_LOAD_ERROR,
)
else:
# skip this question due to image download error
self.questions_container.show_next_question()
return
# load picture from backend response
picture_bytes = b64decode((self.picture_thread.result["image"]).encode())
picture = ImageTk.BytesIO(picture_bytes)
pil_image = Image.open(picture)
max_height = 320
if pil_image.size[1] > max_height:
# picture size is greater then max height, then resize it
percent = max_height / float(pil_image.size[1])
width_size = int((float(pil_image.size[0]) * float(percent)))
pil_image = pil_image.resize((width_size, max_height), Image.ANTIALIAS)
self.picture = ImageTk.PhotoImage(pil_image)
self.picture_label.config(image=self.picture)
self.init_time = datetime.now()
self.after(
1000, check_user_activity, self, self.result_text, USER_INACTIVITY_SECONDS
)
class OpenDoor(Thread):
"""Async door open - relay switcher"""
def __init__(self, **kwargs):
super(OpenDoor, self).__init__(**kwargs)
def run(self):
"""Open door, wait for timeout and then close door"""
print("start")
GPIO.output(RELAY_OUTPUT_PIN, GPIO.LOW)
time.sleep(DOOR_OPEN_SECONDS)
GPIO.output(RELAY_OUTPUT_PIN, GPIO.HIGH)
print("END")
class QuestionsContainer:
"""Questions controller, contains all question frames and can switch between them"""
def __init__(self, controller, student_name, questions):
self.controller = controller
self.student_name = student_name
self.questions = []
self.current_question = -1
for i, question in enumerate(questions):
answers = [
{"answer": question["wrong_answer_1"], "correct": False},
{"answer": question["wrong_answer_2"], "correct": False},
{"answer": question["wrong_answer_3"], "correct": False},
{"answer": question["right_answer"], "correct": True},
]
# show answers in random order
random.shuffle(answers)
# create frame for ech question
self.questions.append(
QuestionPage(
parent=controller,
questions_container=self,
question_text=question["question"],
subject_name=question["subject_name"],
picture_id=question["picture_id"],
answers=answers,
is_last_question=True if i == len(questions) - 1 else False,
)
)
def show_next_question(self):
"""Switch to another question frame"""
self.current_question += 1
if self.questions[self.current_question].picture_id:
# wait for picture question download if picture is presented
self.questions[self.current_question].picture_thread.join()
self.controller.switch_frame_by_instance(self.questions[self.current_question])
return
Functions
def check_user_activity(active_page, ui_element=None, seconds=None)-
Test if frame was changed by user and if timeout expired
Args
active_page- frame page instance
ui_element- optional ui element for text change
seconds- inactivity timeout
Expand source code
def check_user_activity(active_page, ui_element=None, seconds=None): """Test if frame was changed by user and if timeout expired Args: active_page: frame page instance ui_element: optional ui element for text change seconds: inactivity timeout """ if active_page.user_did_action: # user did action then finish periodic check return else: # test if timeout expired if datetime.now() > active_page.init_time + timedelta( seconds=USER_INACTIVITY_SECONDS ): # switch to start page active_page.parent.switch_frame_by_class(ReadCardPage) else: # change text and plan next activity test if ui_element and seconds: seconds -= 1 ui_element.config(text=str(seconds)) active_page.after( 1000, check_user_activity, active_page, ui_element, seconds ) def read_card(read_card_label, controller)-
Read card periodic controller, read card and if is not presented plan another read. If card number is red then frame is switched.
Args
read_card_label:tk.Label- Label for which is planned after action
controller:GUI- class for switch to another frame
Expand source code
def read_card(read_card_label, controller): """Read card periodic controller, read card and if is not presented plan another read. If card number is red then frame is switched. Args: read_card_label (tk.Label): Label for which is planned after action controller (GUI): class for switch to another frame """ try: card_number = read_card_id() except ReadCardError: read_card_label.after(200, read_card, read_card_label, controller) else: controller.switch_frame_by_class(VerifyCardPage, card_number=card_number) return def read_card_id()-
Read Card ID from sensor
Returns
str- Card number
Raises
ReadCardError- when card is not presented
Expand source code
def read_card_id(): """Read Card ID from sensor Returns: str: Card number Raises: ReadCardError: when card is not presented """ card_reader = MFRC522.MFRC522() (status, TagType) = card_reader.MFRC522_Request(card_reader.PICC_REQIDL) if status != card_reader.MI_OK: card_reader.close_spi() raise ReadCardError("Error when reading card.") (status, uid) = card_reader.MFRC522_Anticoll() card_reader.close_spi() if status == card_reader.MI_OK: return f"{uid[0]:02x}{uid[1]:02x}{uid[2]:02x}{uid[3]:02x}".upper() raise ReadCardError("Error when reading card.")
Classes
class ErrorPage (parent, *args, **kwargs)-
Error message frame
Construct a frame widget with the parent MASTER.
Valid resource names: background, bd, bg, borderwidth, class, colormap, container, cursor, height, highlightbackground, highlightcolor, highlightthickness, relief, takefocus, visual, width.
Expand source code
class ErrorPage(tk.Frame, ActionInterface): """Error message frame""" def __init__(self, parent, *args, **kwargs): super(ErrorPage, self).__init__(parent) self.parent = parent self.user_did_action = False self.init_time = None # configure grid layout self.grid_columnconfigure(0, weight=1) self.grid_columnconfigure(1, weight=1) for i in range(5): self.grid_rowconfigure(i, weight=1, uniform="r") # set error type and labels positions error_texts = ERROR_TEXT[kwargs["error_type"].value] error_type_value_label = self.create_label( f"{error_texts['error_name'].format(error_code=kwargs.get('error_code'))}" ) error_type_key_label = self.create_label("Typ chyby:") error_type_key_label.grid(column=0, row=0, stick="e", ipadx=50, ipady=50) error_type_value_label.grid(column=1, row=0, stick="w", ipadx=50, ipady=50) next_step_value_label = self.create_label(f"{error_texts['hint']}") next_step_value_label.grid(column=1, row=1, stick="w", ipadx=50, ipady=50) nex_step_key_label = self.create_label("Postup:") nex_step_key_label.grid(column=0, row=1, stick="e", ipadx=50, ipady=50) contact_value_label = self.create_label(f"{error_texts['contact']}") contact_value_label.grid(column=1, row=2, stick="w", ipadx=50, ipady=50) contact_key_label = self.create_label("Kontakt:") contact_key_label.grid(column=0, row=2, stick="e", ipadx=50, ipady=50) device_id_value_label = self.create_label(f"{DEVICE_ID}") device_id_value_label.grid(column=1, row=3, stick="w", ipadx=50, ipady=50) device_id_key_label = self.create_label("ID zařízení:") device_id_key_label.grid(column=0, row=3, stick="e", ipadx=50, ipady=50) exit_button = tk.Button( self, text="Zavřít", command=lambda: self.close_window(), padx=15, pady=10, font=tkFont.Font(family="Helvetica", size=22), ) exit_button.grid(column=0, row=4, ipadx=30, ipady=30, columnspan=2, stick="n") def create_label(self, text): """Create label with given text Args: text(str): Text in label Returns (tk.Label): configured label """ return tk.Label( self, text=text, fg="black", wraplength=870, justify=tk.CENTER, font=tkFont.Font(family="Helvetica", size=22), ) def close_window(self): """Exit button action, set user activity and switch frame""" self.user_did_action = True self.parent.switch_frame_by_class(ReadCardPage) def post_init_actions(self): """Set init time for user inactivity and start monitoring of this activity""" self.init_time = datetime.now() self.after(1000, check_user_activity, self)Ancestors
- tkinter.Frame
- tkinter.Widget
- tkinter.BaseWidget
- tkinter.Misc
- tkinter.Pack
- tkinter.Place
- tkinter.Grid
- ActionInterface
- abc.ABC
Methods
def close_window(self)-
Exit button action, set user activity and switch frame
Expand source code
def close_window(self): """Exit button action, set user activity and switch frame""" self.user_did_action = True self.parent.switch_frame_by_class(ReadCardPage) def create_label(self, text)-
Create label with given text
Args
text(str): Text in label Returns (tk.Label): configured label
Expand source code
def create_label(self, text): """Create label with given text Args: text(str): Text in label Returns (tk.Label): configured label """ return tk.Label( self, text=text, fg="black", wraplength=870, justify=tk.CENTER, font=tkFont.Font(family="Helvetica", size=22), ) def post_init_actions(self)-
Set init time for user inactivity and start monitoring of this activity
Expand source code
def post_init_actions(self): """Set init time for user inactivity and start monitoring of this activity""" self.init_time = datetime.now() self.after(1000, check_user_activity, self)
class OpenDoor (**kwargs)-
Async door open - relay switcher
This constructor should always be called with keyword arguments. Arguments are:
group should be None; reserved for future extension when a ThreadGroup class is implemented.
target is the callable object to be invoked by the run() method. Defaults to None, meaning nothing is called.
name is the thread name. By default, a unique name is constructed of the form "Thread-N" where N is a small decimal number.
args is the argument tuple for the target invocation. Defaults to ().
kwargs is a dictionary of keyword arguments for the target invocation. Defaults to {}.
If a subclass overrides the constructor, it must make sure to invoke the base class constructor (Thread.init()) before doing anything else to the thread.
Expand source code
class OpenDoor(Thread): """Async door open - relay switcher""" def __init__(self, **kwargs): super(OpenDoor, self).__init__(**kwargs) def run(self): """Open door, wait for timeout and then close door""" print("start") GPIO.output(RELAY_OUTPUT_PIN, GPIO.LOW) time.sleep(DOOR_OPEN_SECONDS) GPIO.output(RELAY_OUTPUT_PIN, GPIO.HIGH) print("END")Ancestors
- threading.Thread
Methods
def run(self)-
Open door, wait for timeout and then close door
Expand source code
def run(self): """Open door, wait for timeout and then close door""" print("start") GPIO.output(RELAY_OUTPUT_PIN, GPIO.LOW) time.sleep(DOOR_OPEN_SECONDS) GPIO.output(RELAY_OUTPUT_PIN, GPIO.HIGH) print("END")
class QuestionPage (parent, questions_container, question_text, subject_name, picture_id, answers, is_last_question, *args, **kwargs)-
Question page
Construct a frame widget with the parent MASTER.
Valid resource names: background, bd, bg, borderwidth, class, colormap, container, cursor, height, highlightbackground, highlightcolor, highlightthickness, relief, takefocus, visual, width.
Expand source code
class QuestionPage(tk.Frame, ActionInterface): """Question page""" def __init__( self, parent, questions_container, question_text, subject_name, picture_id, answers, is_last_question, *args, **kwargs, ): super(QuestionPage, self).__init__(parent) self.parent = parent self.picture_id = picture_id self.is_last_question = is_last_question self.questions_container = questions_container self.user_did_action = False self.init_time = None self.picture = None self.picture_label = None self.picture_thread = None # set two column self.grid_columnconfigure(0, weight=1, uniform="a") self.grid_columnconfigure(1, weight=1, uniform="a") if picture_id: # start picture download self.picture_thread = AsyncBackendCommunicator( url=PICTURE_URL.format(picture_id=picture_id), daemon=True ) self.picture_thread.start() # set row layout for question with picture self.grid_rowconfigure(0, weight=1, uniform="b") self.grid_rowconfigure(1, weight=4, uniform="b") self.grid_rowconfigure(2, weight=10, uniform="b") self.grid_rowconfigure(3, weight=1, uniform="b") self.grid_rowconfigure(4, weight=4, uniform="b") self.grid_rowconfigure(5, weight=4, uniform="b") else: # set row layout for question without picture self.grid_rowconfigure(0, weight=1, uniform="b") self.grid_rowconfigure(1, weight=8, uniform="b") self.grid_rowconfigure(2, weight=1, uniform="b") self.grid_rowconfigure(3, weight=4, uniform="b") self.grid_rowconfigure(4, weight=4, uniform="b") self.questions_mapping = {} for i in range(4): # set answers by button numbers self.questions_mapping[i] = answers[i] self.student_subject_label = tk.Label( self, text=f"{self.questions_container.student_name} - {subject_name}", fg="black", font=tkFont.Font(family="Helvetica", size=18, weight="bold"), padx=20, wraplength=1100, justify=tk.CENTER, ) self.student_subject_label.grid(column=0, row=0, columnspan=2, sticky="nsew") self.question_text_label = tk.Label( self, text=question_text, fg="black", font=tkFont.Font(family="Helvetica", size=22, weight="bold"), wraplength=1100, justify=tk.CENTER, bg="white", ) self.question_text_label.grid(column=0, row=1, columnspan=2, sticky="nsew") self.result_text = tk.Label( self, text=USER_INACTIVITY_SECONDS, fg="black", font=tkFont.Font(family="Helvetica", size=22, weight="bold"), wraplength=1000, justify=tk.CENTER, bg="white", ) self.result_text.grid( column=0, row=3 if self.picture_id else 2, columnspan=2, sticky="nsew" ) if self.picture_id: self.picture_label = tk.Label(self) self.picture_label.grid(column=0, row=2, columnspan=2, sticky="nsew") # set buttons with their texts and actions self.button_0 = self.create_button(0, self.questions_mapping[0]["answer"]) self.questions_mapping[0]["button"] = self.button_0 self.button_0.grid(column=0, row=4 if self.picture_id else 3, sticky="nsew") self.button_1 = self.create_button(1, self.questions_mapping[1]["answer"]) self.questions_mapping[1]["button"] = self.button_1 self.button_1.grid(column=0, row=5 if self.picture_id else 4, sticky="nsew") self.button_2 = self.create_button(2, self.questions_mapping[2]["answer"]) self.questions_mapping[2]["button"] = self.button_2 self.button_2.grid(column=1, row=4 if self.picture_id else 3, sticky="nsew") self.button_3 = self.create_button(3, self.questions_mapping[3]["answer"]) self.button_3.grid(column=1, row=5 if self.picture_id else 4, sticky="nsew") self.questions_mapping[3]["button"] = self.button_3 def create_button(self, button_id, text): """Create configured button Args: button_id (int): button id text (str): button text Returns: tk.Button: Button with text and set click action """ return tk.Button( self, text=text, command=lambda: self.handle_button_click(button_id), wraplength=500, font=tkFont.Font(family="Helvetica", size=16), justify=tk.CENTER, highlightbackground="white", padx=5, pady=5, ) def handle_button_click(self, button_number): """Handle button click, disable all buttons and set colors Args: button_number (int): id of pressed button """ self.user_did_action = True # set flag for user action if self.questions_mapping[button_number]["correct"] is True: # correct answer pressed, open door self.disable_buttons() self.open_door(text="Správná odpověď, dveře otevřeny.") else: # set red color for wrong answer (self.questions_mapping[button_number]["button"]).config( bg="red", highlightbackground="red" ) self.disable_buttons() if self.is_last_question: # last question, open door self.open_door(text="Chybná odpověď, zlepšete se. Dveře otevřeny.") else: # show next question self.render_result_label(text="Chybná odpověď, následuje další otázka.") self.after( (QUESTION_SWITCH_SECONDS * 1000), lambda: self.questions_container.show_next_question(), ) def disable_buttons(self): """Disable click for all buttons and set green color for correct answer""" for i, _ in enumerate(self.questions_mapping): (self.questions_mapping[i]["button"]).config( state="disabled", disabledforeground="black" ) if self.questions_mapping[i]["correct"] is True: (self.questions_mapping[i]["button"]).config( bg="green", highlightbackground="green" ) def open_door(self, text): """Open door and set result text and plan frame switch""" self.render_result_label(text=text) self.after( (QUESTION_SWITCH_SECONDS * 1000), lambda: self.parent.switch_frame_by_class(ReadCardPage), ) OpenDoor().start() def render_result_label(self, text): """Set text for result label Args: text (str): text in label """ self.result_text.config(text=text) def post_init_actions(self): """Render picture if question contain it and set init time and start user inactivity timeout""" if self.picture_id: if self.picture_thread.result is None: # question download failed if self.is_last_question: # last question so show error self.parent.switch_frame_by_class( ErrorPage, error_type=Errors.IMAGE_LOAD_ERROR, ) else: # skip this question due to image download error self.questions_container.show_next_question() return # load picture from backend response picture_bytes = b64decode((self.picture_thread.result["image"]).encode()) picture = ImageTk.BytesIO(picture_bytes) pil_image = Image.open(picture) max_height = 320 if pil_image.size[1] > max_height: # picture size is greater then max height, then resize it percent = max_height / float(pil_image.size[1]) width_size = int((float(pil_image.size[0]) * float(percent))) pil_image = pil_image.resize((width_size, max_height), Image.ANTIALIAS) self.picture = ImageTk.PhotoImage(pil_image) self.picture_label.config(image=self.picture) self.init_time = datetime.now() self.after( 1000, check_user_activity, self, self.result_text, USER_INACTIVITY_SECONDS )Ancestors
- tkinter.Frame
- tkinter.Widget
- tkinter.BaseWidget
- tkinter.Misc
- tkinter.Pack
- tkinter.Place
- tkinter.Grid
- ActionInterface
- abc.ABC
Methods
-
Create configured button
Args
button_id:int- button id
text:str- button text
Returns
tk.Button- Button with text and set click action
Expand source code
def create_button(self, button_id, text): """Create configured button Args: button_id (int): button id text (str): button text Returns: tk.Button: Button with text and set click action """ return tk.Button( self, text=text, command=lambda: self.handle_button_click(button_id), wraplength=500, font=tkFont.Font(family="Helvetica", size=16), justify=tk.CENTER, highlightbackground="white", padx=5, pady=5, ) -
Disable click for all buttons and set green color for correct answer
Expand source code
def disable_buttons(self): """Disable click for all buttons and set green color for correct answer""" for i, _ in enumerate(self.questions_mapping): (self.questions_mapping[i]["button"]).config( state="disabled", disabledforeground="black" ) if self.questions_mapping[i]["correct"] is True: (self.questions_mapping[i]["button"]).config( bg="green", highlightbackground="green" ) -
Handle button click, disable all buttons and set colors
Args
button_number:int- id of pressed button
Expand source code
def handle_button_click(self, button_number): """Handle button click, disable all buttons and set colors Args: button_number (int): id of pressed button """ self.user_did_action = True # set flag for user action if self.questions_mapping[button_number]["correct"] is True: # correct answer pressed, open door self.disable_buttons() self.open_door(text="Správná odpověď, dveře otevřeny.") else: # set red color for wrong answer (self.questions_mapping[button_number]["button"]).config( bg="red", highlightbackground="red" ) self.disable_buttons() if self.is_last_question: # last question, open door self.open_door(text="Chybná odpověď, zlepšete se. Dveře otevřeny.") else: # show next question self.render_result_label(text="Chybná odpověď, následuje další otázka.") self.after( (QUESTION_SWITCH_SECONDS * 1000), lambda: self.questions_container.show_next_question(), ) def open_door(self, text)-
Open door and set result text and plan frame switch
Expand source code
def open_door(self, text): """Open door and set result text and plan frame switch""" self.render_result_label(text=text) self.after( (QUESTION_SWITCH_SECONDS * 1000), lambda: self.parent.switch_frame_by_class(ReadCardPage), ) OpenDoor().start() def post_init_actions(self)-
Render picture if question contain it and set init time and start user inactivity timeout
Expand source code
def post_init_actions(self): """Render picture if question contain it and set init time and start user inactivity timeout""" if self.picture_id: if self.picture_thread.result is None: # question download failed if self.is_last_question: # last question so show error self.parent.switch_frame_by_class( ErrorPage, error_type=Errors.IMAGE_LOAD_ERROR, ) else: # skip this question due to image download error self.questions_container.show_next_question() return # load picture from backend response picture_bytes = b64decode((self.picture_thread.result["image"]).encode()) picture = ImageTk.BytesIO(picture_bytes) pil_image = Image.open(picture) max_height = 320 if pil_image.size[1] > max_height: # picture size is greater then max height, then resize it percent = max_height / float(pil_image.size[1]) width_size = int((float(pil_image.size[0]) * float(percent))) pil_image = pil_image.resize((width_size, max_height), Image.ANTIALIAS) self.picture = ImageTk.PhotoImage(pil_image) self.picture_label.config(image=self.picture) self.init_time = datetime.now() self.after( 1000, check_user_activity, self, self.result_text, USER_INACTIVITY_SECONDS ) def render_result_label(self, text)-
Set text for result label
Args
text:str- text in label
Expand source code
def render_result_label(self, text): """Set text for result label Args: text (str): text in label """ self.result_text.config(text=text)
class QuestionsContainer (controller, student_name, questions)-
Questions controller, contains all question frames and can switch between them
Expand source code
class QuestionsContainer: """Questions controller, contains all question frames and can switch between them""" def __init__(self, controller, student_name, questions): self.controller = controller self.student_name = student_name self.questions = [] self.current_question = -1 for i, question in enumerate(questions): answers = [ {"answer": question["wrong_answer_1"], "correct": False}, {"answer": question["wrong_answer_2"], "correct": False}, {"answer": question["wrong_answer_3"], "correct": False}, {"answer": question["right_answer"], "correct": True}, ] # show answers in random order random.shuffle(answers) # create frame for ech question self.questions.append( QuestionPage( parent=controller, questions_container=self, question_text=question["question"], subject_name=question["subject_name"], picture_id=question["picture_id"], answers=answers, is_last_question=True if i == len(questions) - 1 else False, ) ) def show_next_question(self): """Switch to another question frame""" self.current_question += 1 if self.questions[self.current_question].picture_id: # wait for picture question download if picture is presented self.questions[self.current_question].picture_thread.join() self.controller.switch_frame_by_instance(self.questions[self.current_question]) returnMethods
def show_next_question(self)-
Switch to another question frame
Expand source code
def show_next_question(self): """Switch to another question frame""" self.current_question += 1 if self.questions[self.current_question].picture_id: # wait for picture question download if picture is presented self.questions[self.current_question].picture_thread.join() self.controller.switch_frame_by_instance(self.questions[self.current_question]) return
class ReadCardPage (parent, *args, **kwargs)-
Read card page
Construct a frame widget with the parent MASTER.
Valid resource names: background, bd, bg, borderwidth, class, colormap, container, cursor, height, highlightbackground, highlightcolor, highlightthickness, relief, takefocus, visual, width.
Expand source code
class ReadCardPage(tk.Frame, ActionInterface): """Read card page""" def __init__(self, parent, *args, **kwargs): super(ReadCardPage, self).__init__(parent) self.parent = parent self.read_card_label = tk.Label( # Read label self, text="Přiložte vstupní kartu", fg="black", font=tkFont.Font(family="Helvetica", size=45, weight="bold"), ) # Center label position and grid settings self.read_card_label.grid(column=1, row=1, sticky="nsew") self.grid_rowconfigure(1, weight=1) self.grid_columnconfigure(1, weight=1) def post_init_actions(self): """Plan periodic card read""" read_card(self.read_card_label, self.parent)Ancestors
- tkinter.Frame
- tkinter.Widget
- tkinter.BaseWidget
- tkinter.Misc
- tkinter.Pack
- tkinter.Place
- tkinter.Grid
- ActionInterface
- abc.ABC
Methods
def post_init_actions(self)-
Plan periodic card read
Expand source code
def post_init_actions(self): """Plan periodic card read""" read_card(self.read_card_label, self.parent)
class VerifyCardPage (parent, *args, **kwargs)-
Verify card number page
Construct a frame widget with the parent MASTER.
Valid resource names: background, bd, bg, borderwidth, class, colormap, container, cursor, height, highlightbackground, highlightcolor, highlightthickness, relief, takefocus, visual, width.
Expand source code
class VerifyCardPage(tk.Frame, ActionInterface): """Verify card number page""" def __init__(self, parent, *args, **kwargs): super(VerifyCardPage, self).__init__(parent) self.parent = parent self.card_number = kwargs.get("card_number") # Info label self.verify_card_label = tk.Label( self, text="Karta načtena. Probíhá ověřování její platnosti.", fg="black", font=tkFont.Font(family="Helvetica", size=45, weight="bold"), wraplength=1000, justify=tk.CENTER, ) self.verify_card_label.grid(column=1, row=1) self.grid_rowconfigure(1, weight=1) self.grid_columnconfigure(1, weight=1) def post_init_actions(self): """Start async backend call and start monitoring if this operation finish""" backend_thread = AsyncBackendCommunicator( url=QUESTIONS_URL, daemon=True, params=dict( card_number=self.card_number, question_count=NUMBER_OF_QUESTIONS, device_id=DEVICE_ID, ), ) backend_thread.start() self.monitor_backend_thread(backend_thread) def monitor_backend_thread(self, thread): """Check if async thread is finished. If it is then handle result and switch frame if it is not finished then plan periodic result check. Args: thread (AsyncBackendCommunicator): Started backend call thread """ if thread.is_alive(): # thread still working, plan periodic check after 100ms self.after(100, lambda: self.monitor_backend_thread(thread)) else: if thread.is_result_ok: # thread finished and questions were downloaded if not thread.result.get("questions"): self.parent.switch_frame_by_class( ErrorPage, error_type=Errors.NO_QUESTIONS_ERROR, ) return # init questions container and show first question questions_container = QuestionsContainer( controller=self.parent, student_name=thread.result["student_name"], questions=thread.result["questions"], ) questions_container.show_next_question() return else: # thread finished with error if thread.error_code == 404: error_type = Errors.NO_ACTIVE_STUDY_ERROR elif thread.error_code == 403: error_type = Errors.NO_ACCESS_ERROR else: error_type = thread.error_type # show error page self.parent.switch_frame_by_class( ErrorPage, error_type=error_type, error_code=thread.error_code, ) returnAncestors
- tkinter.Frame
- tkinter.Widget
- tkinter.BaseWidget
- tkinter.Misc
- tkinter.Pack
- tkinter.Place
- tkinter.Grid
- ActionInterface
- abc.ABC
Methods
def monitor_backend_thread(self, thread)-
Check if async thread is finished. If it is then handle result and switch frame if it is not finished then plan periodic result check.
Args
thread:AsyncBackendCommunicator- Started backend call thread
Expand source code
def monitor_backend_thread(self, thread): """Check if async thread is finished. If it is then handle result and switch frame if it is not finished then plan periodic result check. Args: thread (AsyncBackendCommunicator): Started backend call thread """ if thread.is_alive(): # thread still working, plan periodic check after 100ms self.after(100, lambda: self.monitor_backend_thread(thread)) else: if thread.is_result_ok: # thread finished and questions were downloaded if not thread.result.get("questions"): self.parent.switch_frame_by_class( ErrorPage, error_type=Errors.NO_QUESTIONS_ERROR, ) return # init questions container and show first question questions_container = QuestionsContainer( controller=self.parent, student_name=thread.result["student_name"], questions=thread.result["questions"], ) questions_container.show_next_question() return else: # thread finished with error if thread.error_code == 404: error_type = Errors.NO_ACTIVE_STUDY_ERROR elif thread.error_code == 403: error_type = Errors.NO_ACCESS_ERROR else: error_type = thread.error_type # show error page self.parent.switch_frame_by_class( ErrorPage, error_type=error_type, error_code=thread.error_code, ) return def post_init_actions(self)-
Start async backend call and start monitoring if this operation finish
Expand source code
def post_init_actions(self): """Start async backend call and start monitoring if this operation finish""" backend_thread = AsyncBackendCommunicator( url=QUESTIONS_URL, daemon=True, params=dict( card_number=self.card_number, question_count=NUMBER_OF_QUESTIONS, device_id=DEVICE_ID, ), ) backend_thread.start() self.monitor_backend_thread(backend_thread)